#!/usr/bin/env python
# encoding: utf-8
"""
infinitest.py

Created by Ben Rady on 2007-08-28.
Copyright (c) 2007 Ben Rady. All rights reserved.
"""

from stat import *
import sys
import os
import unittest
import time
import traceback

class InfinitestCore:
    def __init__(self, project_dir):
        if not os.path.isdir(project_dir):
            raise Exception("Not a directory: "+project_dir)
        self.dir = project_dir
        self.file_modification_times = {}
        self.observers = []

    def _findFiles(self):
        files = [ os.path.join(root, file) 
                for (root, ignored, files) in os.walk(self.dir)
                for file in files  if file.endswith("py")
               ]
        return sorted(files)

    def _findTestFiles(self):
        return [ name for name in self._findFiles() 
                 if name.lower().startswith('test') 
                 or name.lower().endswith('test.py') ]

    def _collectModTimes(self, files):
        return [ (file, os.stat(file)[ST_MTIME]) for file in files ]

    def _genFileModificationList(self):
        files = self._findFiles()
        return self._collectModTimes(files)

    def filesHaveChanged(self):
        oldlist = self.file_modification_times
        newlist =  self._genFileModificationList()
        #self._report_changes(oldlist, newlist) # Just for debugging purposes
        self.file_modification_times = newlist # TODO: Command/query separation?
        return oldlist != newlist

    def _report_changes(self,oldlist,newlist):
        deleted = filter( lambda x: x not in newlist, oldlist)
        for file in deleted:
            print "-", file
        added = filter(lambda x: x not in oldlist, newlist)
        for file in added:
            print "+", file

    def getModuleForTest(self, test_file):
        commonpath, module_path = self.relativeModulePathAndFile(test_file)
        hold_syspath = sys.path
        try:
            sys.path.append(self.dir)
            sys.path.append(commonpath)
            module =  __import__(module_path, {}, {}, [''])
            return module
        finally:
            sys.path = hold_syspath

    def relativeModulePathAndFile(self, filename):
        filename = os.path.normpath(filename)
        commonprefix = os.path.commonprefix([self.dir, filename])
        if commonprefix:
            filename = filename[len(commonprefix):].lstrip(os.sep)
        name, ext = os.path.splitext(filename)
        module_path = name.split('/')
        return commonprefix, '.'.join(module_path)

    def runTests(self):
        results = []
        for test_file in self._findTestFiles():
            result = unittest.TestResult()
            try:
                test_module = self.getModuleForTest(test_file)
                reload(test_module)
                suite = unittest.TestLoader().loadTestsFromModule(test_module)
                suite.run(result=result)
            except SyntaxError, e:
                result = SyntaxErrorResult(e)
            results.append(result)
        return results

    # Observer pattern for test results seems reasonable
    def subscribe(self, other):
        self.observers.append(other)
    def publish(self, results):
        for observer in self.observers:
            observer(results)

    def update(self):
        if self.filesHaveChanged():
            results = self.runTests() 
            self.publish(results)

class TextResultPrinter(object):
    def __call__(self, results):
        self.printResults(results)

    def printHeader(self):
        now = time.ctime() 
        print 
        print (" "+now+" ").center(70,"=")
    def printFooter(self, text):
        print "-"*70
        print text
        print
    def printResults(self, test_results):
        tests_passed = True
        self.printHeader()
        for result in test_results:
            if not result.wasSuccessful():
                self.printStackTraces(result.failures, "FAILURES")
                self.printStackTraces(result.errors, "ERRORS")

                self.printFooter(
                    "Ran %d tests at %s.  ERRORS: %d, FAILURES:%d" % (
                    result.testsRun, 
                    time.asctime(),
                    len(result.errors), 
                    len(result.failures)
                    )
                )
                tests_passed = False
        if tests_passed:
            self.printFooter("All tests passed")

    def printStackTraces(self, method_results, name):
        if method_results:
            print name + ": "
            for method,trace in method_results:
                for line in str(trace).split("\n"):
                    print "   >",line

class SyntaxErrorResult():
    def __init__(self, syntax_error):
        self.error = syntax_error;
        self.failures = []
        self.errors = [("unknown", syntax_error)]
        self.testsRun = 0

    def wasSuccessful(self):
        return False

class RecurringTimer:
    def sleep_for_ms(self, ms):
        time.sleep(ms/1000.0)
    def currentTimeInMs(self):
        return time.time()*1000

    def __init__(self, timer_callback, cycle_time_in_ms=1500, num_cycles=0):
        self.callback = timer_callback
        self.cycle_time = cycle_time_in_ms
        self.cycles = num_cycles

    def start(self):
        cycle_count = 0
        while (self.cycles == 0 or self.cycles != cycle_count):
            start_time_ms = self.currentTimeInMs()
            try:
                self.callback()
                elapsed_time = self.currentTimeInMs() - start_time_ms
                sleep_time = self.cycle_time - elapsed_time
                self.sleep_for_ms(sleep_time)
            finally:
                cycle_count += 1

def pathFromUserInput(userPath):
    expanded = os.path.expanduser(userPath)
    realized = os.path.realpath(expanded)
    return realized

if __name__ == '__main__':
    project_dir = "."
    if (len(sys.argv) == 2):
        project_dir = pathFromUserInput(sys.argv[1])
    os.chdir(project_dir)
    sys.path.append(".")
    infinitest = InfinitestCore(project_dir)
    infinitest.subscribe(TextResultPrinter())
    timer = RecurringTimer(infinitest.update, cycle_time_in_ms = 5000)
    print "Starting Infinitest..."
    print str(len(infinitest._findFiles())) + " files found under directory " + project_dir
    timer.start()

